Extract Tasks into the ModelContextProtocol.Extensions.Tasks extension package#1693
Merged
jeffhandley merged 11 commits intoJul 14, 2026
Conversation
- Add ResultOrAlternate<TResult> replacing task-specific types in server pipeline - Add McpServerRequestHandler for custom request handler registration (seam #1) - Add McpServerOptions.RequestHandlers property with wiring in McpServerImpl - Rename CallToolWithTaskHandler/Filters to CallToolWithAlternateHandler/Filters - Rename SetTaskAugmented to SetWithAlternate (remove tasks/get guard) - Rename InvokeToolAsTask to InvokeToolWithAlternate - Rename BuildInitialTaskToolFilter to BuildInitialAlternateToolFilter - Make McpClient.ResolveInputRequestsAsync public (seam #4) - Update test references to use new names - Adapt TaskHandlerConfigurationValidationTests for removed guard Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Create src/ModelContextProtocol.Extensions.Tasks/ with csproj, JSON context, server builder extensions, client extension methods - Move task protocol DTOs (CreateTaskResult, GetTaskResult, UpdateTask*, CancelTask*, McpTaskStatus, TaskStatusNotificationParams) to extension - Move server types (IMcpTaskStore, InMemoryMcpTaskStore, McpTaskInfo, InputResponseReceivedEventArgs) to extension - Move task constants (RequestMethods.Tasks*, NotificationMethods.TaskStatus*, MetaKeys.RelatedTask, McpExtensions.Tasks) into TasksProtocol static class - Delete McpTaskExecutionContext, ResultOrCreatedTask from Core - Remove ~18 task [JsonSerializable] entries from McpJsonUtilities - Remove McpServerOptions.TaskStore and McpClientOptions.MaxConsecutiveStuckPolls - Remove task client methods from McpClient (CallToolRawAsync, PollTaskToCompletion, GetTaskAsync, UpdateTaskAsync, CancelTaskAsync, GetMetaWithTaskCapability, ThrowIfTasksNotSupported) - Remove task server methods/handlers (GetTaskHandler, UpdateTaskHandler, CancelTaskHandler, ConfigureTasks, InvokeToolAsTask, task cancellation sources) - Add Tasks_DiagnosticId (MCPEXP001) to Experimentals.cs - Extension WithTasks(store) registers request handlers via seam #1, alternate filter via seam #2, interceptor via seam #3 - Extension client methods: CallToolAsTaskAsync, CallToolWithPollingAsync, GetTaskAsync, UpdateTaskAsync, CancelTaskAsync - Manually serialize/deserialize InputResponses in UpdateTaskAsync to handle internal Core property visibility across assembly boundary - Update test project and TasksExtension sample to reference new package - Update all task test files with new using and extension API Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Restore the inline-result branch comment on the !raw.IsTask path - Restore the CompletedTaskResult JsonElement deserialization comment - Restore the RunReport sleep-vs-real-work comment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
added 6 commits
July 8, 2026 13:41
- Reject custom RequestHandlers that collide with built-in or duplicate methods - Throw when an outgoing-request interceptor returns no result for sampling/roots - Guard the Tasks background execution against unobserved store exceptions and log failures - Add collision-guard tests for custom request handlers
Discard tasks once their advertised time-to-live elapses: expired tasks are removed on access and a throttled opportunistic sweep reclaims expired tasks that are never polled again. A null or non-positive TimeToLive keeps tasks for the process lifetime, so the default behavior is unchanged.
Improve the InvalidOperationException thrown when CallToolFilters and CallToolWithAlternateFilters are both configured, naming the common indirect cause (combining AddAuthorizationFilters() with WithTasks()) and the current limitation. Add filter property doc notes and tests.
The Tasks feature moved from ModelContextProtocol.Core to the new ModelContextProtocol.Extensions.Tasks package, removing several public APIs from Core. Regenerated the baseline suppression entries so package validation against the 1.3.0 baseline passes.
The Tasks feature moved from ModelContextProtocol.Core to the new ModelContextProtocol.Extensions.Tasks package, which renamed several types and members. Updated docs/concepts/tasks/tasks.md and the task references in docs/concepts/stateless/stateless.md so the docfx cross references resolve and the samples match the current API: - Types moved to the ModelContextProtocol.Extensions.Tasks namespace. - Server enablement uses WithTasks(IMcpTaskStore) instead of the removed McpServerOptions.TaskStore property. - Custom tool handlers use CallToolWithAlternateHandler returning ResultOrAlternate<CallToolResult>. - Task scope uses the McpTasksServerExtensions.CreateMcpTaskScope extension. - Client calls use CallToolWithPollingAsync, CallToolAsTaskAsync, and the Get/Update/CancelTaskAsync client extensions; the stuck-poll threshold is now the maxConsecutiveStuckPolls parameter.
jeffhandley
commented
Jul 9, 2026
jeffhandley
left a comment
Contributor
Author
There was a problem hiding this comment.
The hardening looks good to me, @tarekgh. We're inconsistent with throw helpers and I see that we have some places where we ifdef around them to use then on .NET but not on netfx. So you can disregard my 2 comments below as low-value suggestions that would just result in ifdef'ing, which doesn't improve the readability anyway.
I cannot approve since I started the PR, but consider me approved.
3f77adb to
3414c56
Compare
tarekgh
previously approved these changes
Jul 13, 2026
halter73
reviewed
Jul 14, 2026
Contributor
|
I filed #1704 to describe some follow up work that I've started on that I don't think needs to be part of this PR. I was on the fence regarding the split, but I think it's probably a good idea. |
- Mark ResultOrAlternate<T>, CallToolWithAlternateHandler, and CallToolWithAlternateFilters as [Experimental(MCPEXP002)] - Set ResultType = "task" in the CreateTaskResult constructor and drop the redundant assignment in ToCreateTaskResult - Replace the public untyped ResultOrAlternate constructor with a typed FromAlternate<TAlternate>(TAlternate, JsonTypeInfo<TAlternate>) factory and make the untyped constructor private - Normalize a returned InputRequiredResult through the alternate path with a thrown InputRequiredException in the MRTR backcompat resolver - Add MRTR tests for the returned-result form (server-side backcompat resolution and native MRTR round-trip) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
tarekgh
approved these changes
Jul 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Extracts the SEP-2663 Tasks feature out of
ModelContextProtocol.Coreinto a newbolt-on package,
ModelContextProtocol.Extensions.Tasks, mirroring the existingModelContextProtocol.Extensions.Apps. Core is left with no compile-time knowledge ofTasks: the extension references the main package and layers Tasks behavior in from the
side.
Unlike Apps -- which was purely additive metadata over existing public APIs -- Tasks was
woven into Core's request-dispatch pipeline (task-augmented
tools/call, redirection ofserver-initiated
sampling/elicitation/roots, and transparent client polling). To makea clean side bolt-on possible, Core gains a small set of generic, typed seams. There is no
InternalsVisibleTo; the extension consumes only public Core API.All of the new Core extensibility seams are marked
[Experimental](MCPEXP002) -- they arepreview extensibility hooks whose shape is expected to evolve (see #1704). The moved Tasks
feature APIs are non-experimental, consistent with #1642.
New public Core API surface (the seams)
ResultOrAlternate<TResult>-- a typed "result or a server-chosen alternate" envelopeA handler that normally returns
TResultcan instead return an alternateResultsubtype(e.g. the Tasks extension answering
tools/callwith aCreateTaskResult). The alternatecarries its own
JsonTypeInfo, so Core can serialize it without compile-time knowledge ofthe concrete type -- keeping the path Native-AOT friendly.
Typed
tools/callalternate handler + filtersThe extension installs an alternate-result filter that may return a
CreateTaskResult(the"run this tool call as a task" path) while the normal typed
tools/callpipeline ispreserved. The alternate handler/filters are mutually exclusive with the normal
CallToolHandler/CallToolFilters; mixing the two styles is rejected at configuration timewith an actionable error (so, today,
WithTasks()cannot be combined withAddAuthorizationFilters()-- composing them is tracked by #1704). A returned alternate thatis an
InputRequiredResultis normalized by Core's MRTR backcompat resolver exactly like athrown
InputRequiredException, so MRTR keeps working for non-MRTR clients regardless ofwhich form a handler uses.
Generic custom request handlers (for
tasks/*methods)Lets an extension own arbitrary JSON-RPC methods (
tasks/get,tasks/update,tasks/cancel)with full control over request/response serialization. This registry is method-keyed and
generic, so an extension can add more of its own RPCs with no Core change.
Scoped redirection of server-initiated outgoing requests
While a tool runs as a task, its
sampling/elicitation/rootsrequests must be capturedinto the task store instead of sent to the client.
InterceptOutgoingRequestsinstalls aninterceptor and returns an
IDisposablewhose disposal restores the previous interceptor,so the redirection lifetime is explicit and scoped.
Client input-request resolution hook
A public hook that lets the extension's client-side polling loop resolve server input
requests through the registered elicitation/sampling handlers.
Protocol-version gating reuses existing public surface (
McpHttpHeaders.July2026ProtocolVersion/IsJuly2026OrLaterProtocolVersion); no new draft-detection API is added to Core.What moves to the extension
ModelContextProtocol.Extensions.Tasksowns all Tasks-specific surface:CreateTaskResult(whose constructor sets thetaskresult discriminator),GetTaskRequestParams/Result,UpdateTaskRequestParams/Result,CancelTaskRequestParams/Result,TaskStatusNotificationParams,McpTaskStatus,ResultOrCreatedTask<T>.IMcpTaskStore,InMemoryMcpTaskStore,McpTaskExecutionContext,McpTaskInfo,InputResponseReceivedEventArgs.tools/callwrapper, the outgoing-request redirection, and the clientpolling loop -- all re-expressed on top of the Core seams above.
tasks/*request methods,notifications/tasks/status, thetaskresult discriminator,the
RelatedTaskmeta key, thetasksextension capability string, and the Tasks[JsonSerializable]registrations.Public entry points, mirroring Apps:
Design rationale
ResultOrAlternate<TResult>and the typedCallToolWithAlternate*members keep thetools/callalternate path expressed in terms ofreal result types, and the envelope is a reusable, AOT-friendly primitive.
IDisposable(
InterceptOutgoingRequests) that restores the previous interceptor on dispose.InternalsVisibleTo. The extension depends only on public Core API, exactly like Apps.Experimental surface
ResultOrAlternate<T>,CallToolWithAlternateHandler,CallToolWithAlternateFilters,McpServerRequestHandler/McpServerOptions.RequestHandlers,McpServer.InterceptOutgoingRequests, andMcpClient.ResolveInputRequestsAsync-- are allmarked
[Experimental]under the existingMCPEXP002"Subclassing" (SDK-extensibility)diagnostic. They are preview extensibility hooks whose shape is expected to evolve; Make server extension handlers composable with filters and subscriptions #1704
tracks generalizing the
tools/call-specific alternate seam into a method-agnostic one.ExperimentalAttributefrom official Extensions/Tasks/MRTR APIs and alignMCPEXP001usage #1642 (which removed[Experimental]from the official Tasks/MRTR feature APIs).Breaking changes
This is a preview SDK and the change is intentionally breaking:
ModelContextProtocol(.Core)intoModelContextProtocol.Extensions.Tasks; consumers add a package reference and ausing.(
WithTasks(...),client.GetTaskAsync(...), etc.) rather than Core members.Validation
dotnet buildclean (0 warnings / 0 errors) across net10.0/net9.0/net8.0/netstandard2.0with
TreatWarningsAsErrors=true.